https://github.com/rafadls/LPC/tree/main/L6¶pandas con respecto a trabajar en Python 'puro'.El laboratorio deberá ser desarrollado sin el uso indiscriminado de iteradores nativos de python (aka "for", "while"). La idea es que aprendan a exprimir al máximo las funciones optimizadas que nos entrega numpy, las cuales vale mencionar, son bastante más eficientes que los iteradores nativos sobre arreglos (o tensores).
# Libreria Core del lab.
import numpy as np
import pandas as pd
import datetime
from IPython.display import HTML
# Libreria para plotear (En colab esta desactualizado plotly)
!pip install --upgrade plotly
import plotly.express as px
import plotly.graph_objects as go
# Librerias utiles
from sklearn.manifold import TSNE
from sklearn.cluster import KMeans
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import FunctionTransformer
# Para que se vea más limpio
import warnings
warnings.filterwarnings("ignore")
Requirement already up-to-date: plotly in c:\users\rafael\anaconda3\lib\site-packages (5.3.1) Requirement already satisfied, skipping upgrade: tenacity>=6.2.0 in c:\users\rafael\anaconda3\lib\site-packages (from plotly) (6.3.1) Requirement already satisfied, skipping upgrade: six in c:\users\rafael\anaconda3\lib\site-packages (from plotly) (1.15.0)
# Si usted está utilizando Colabolatory le puede ser útil este código para cargar los archivos.
try:
from google.colab import drive
drive.mount("/content/drive")
path = 'Dirección donde tiene los archivos en el Drive'
except:
print('Ignorando conexión drive-colab')
Ignorando conexión drive-colab
Mr. Lepin, en una nueva reunión, le cuenta a ud y su equipo que los resultados derivados del análisis exploratorio de dato presentaron una gran utilidad para la empresa y que tiene un gran entusiasmo por continuar trabajando con ustedes. Es por esto, que Mr. Lepin les pide que cargue y visualicen algunas de las filas que componen el Dataset. A continuación un extracto de lo parlamentado en la reunión:
- Usted: Es un gran logro para nuestro equipo que usted haya encontrado excelente el EDA. ¿Qué tiene en mente ahora?
- Mr. Lepin: Resulta que hace algún tiempo, mientras tomaba un mojito en una reunión de gerentes en Panamá, oí a un *chato* acerca de **LRMFP**, que es un modelo que permite personificar a los clientes a través de la farbicación de distintos atributos que describen a los clientes. Lo encontré es-tu-pendo ñatito.
- Usted: Ehh bueno. Investigaremos acerca de este modelo y veremos lo que podemos hacer.
Por ende, su siguiente tarea es calcular LRMFP sobre cada cliente y luego hacer un análisis de las características generadas. Para esto, el área de ventas les entrega un nuevo archivo llamado online_retail_II_cleaned.pickle, quien posee los datos del DataFrame original limpios y listos para obtener las características solicitadas por Mr. Lepin.
df_retail = pd.read_pickle("online_retail_II_cleaned.pickle")
df_retail = df_retail.astype(
{
"Invoice": "category",
"StockCode": "category",
"Description": "category",
"Description": str,
"Customer ID": "category",
"Country": "category"
}
)
df_retail.head()
| Invoice | StockCode | Description | Quantity | InvoiceDate | Price | Customer ID | Country | |
|---|---|---|---|---|---|---|---|---|
| 0 | 489434 | 85048 | 15CM CHRISTMAS GLASS BALL 20 LIGHTS | 12 | 2009-12-01 07:45:00 | 6.95 | 13085.0 | United Kingdom |
| 1 | 489434 | 79323P | PINK CHERRY LIGHTS | 12 | 2009-12-01 07:45:00 | 6.75 | 13085.0 | United Kingdom |
| 2 | 489434 | 79323W | WHITE CHERRY LIGHTS | 12 | 2009-12-01 07:45:00 | 6.75 | 13085.0 | United Kingdom |
| 3 | 489434 | 22041 | RECORD FRAME 7" SINGLE SIZE | 48 | 2009-12-01 07:45:00 | 2.10 | 13085.0 | United Kingdom |
| 4 | 489434 | 21232 | STRAWBERRY CERAMIC TRINKET BOX | 24 | 2009-12-01 07:45:00 | 1.25 | 13085.0 | United Kingdom |
Como ya se les comento, Mr. Lepin esta interesado en obtener las características LRMFP, para esto les señala que estas características se construyen en base a las siguientes definiciones:
Recency (R): Indica la actualidad de la interacción de un cliente con la empresa, y da información sobre la tendencia a repetir la compra. Se define como: $$Recency(n)=\dfrac{1}{n} \sum^n_{i=1} date\_diff(t_{fecha final}, t_{m-i+1})$$
Donde $date\_diff$ representa la diferencia en días entre la fecha de finalización del periodo de observación ($t_{fecha final}$), y la fecha de una visita del cliente cercana a $t_{fecha final}$, $t_{m-i+1}; t_{m}$ es la última visita del cliente; y n es el número de visitas recientes del cliente consideradas.
Monetary (M): El término "monetario" se refiere a la cantidad media de dinero gastada por cada visita del cliente durante el período de observación y refleja la contribución del cliente a los ingresos de la empresa.
Frequency (F): Se refiere al número total de visitas del cliente durante el periodo de observación. Cuanto mayor sea la frecuencia, mayor será la fidelidad del cliente.
Periodicity (P): Representa si los clientes visitan las tiendas con regularidad.
Donde $IVT$ denota el tiempo entre visitas y n representa el número de valores de tiempo entre visitas de un cliente.
$$IVT_i=date\_diff(t_{i+1},t)$$En base a las definiciones señaladas, diseñe una función que permita obtener las características LRMFP recibiendo un DataFrame como entrada. Para esto, no estará permitido el uso de iteradores, utilice todas las herramientas que les ofrece pandas para realizar esto.
Una referencia que le puede ser útil es el documento original en donde se propone este método.
Nota: Para la $fechafinal$ utilice la fecha máxima del dataset más 1 día.
Ejemplo de Resultado Esperado:
| Customer ID | Length | Recency | Frequency | Monetary | Periodicity |
|---|---|---|---|---|---|
| 12346.0 | 294 | 67 | 46 | -64.68 | 37.0 |
| 12347.0 | 37 | 3 | 71 | 1323.32 | 0.0 |
| 12349.0 | 327 | 43 | 107 | 2646.99 | 78.0 |
| 12352.0 | 16 | 11 | 18 | 343.80 | 0.0 |
| 12356.0 | 44 | 16 | 84 | 3562.25 | 12.0 |
Respuesta:
A continuación se muestra la función para obtener las características LRMFP. Se agrupa en compras (boleta) y luego en clientes, esto se debe a que hay valores que deben ser obtenidos de la compra, como Monetary.
No se utilizan iteradores en el código
def custom_features(dataframe_in):
df_out = dataframe_in[['Invoice','InvoiceDate','Customer ID']]
df_out['Total_Cost'] = dataframe_in['Quantity']*dataframe_in['Price']
# dataset pasa de ser de productos a compras completas (por boleta)
# se juntan por boleta porque queremos precios y tiempos en relación a las compras no a los productos
df_out = df_out.groupby(by=['Invoice']).agg({'InvoiceDate':'first','Customer ID':'first','Total_Cost':'sum'})
df_out.reset_index(inplace=True,drop=True)
last_date = np.max(df_out['InvoiceDate']) + datetime.timedelta(days=1)
def f(x):
d = {}
#Intervalo de tiempo, en días, entre la primera y la última visita del cliente. Mientras mas grande sea el valor, mas fiel es el cliente
d['Length'] = (x['InvoiceDate'].max() - x['InvoiceDate'].min()).days
d['Recency'] = int(np.mean((last_date - x['InvoiceDate']).dt.days))
d['Monetary'] = int(x['Total_Cost'].mean())
d['Frequency'] = len(x['Total_Cost'])
d['Periodicity'] = int(np.std(x['InvoiceDate'].diff()).days) if d['Frequency']>1 else 0
return pd.Series(d, index=['Length', 'Recency', 'Monetary', 'Frequency','Periodicity'])
df_out = df_out.groupby('Customer ID').apply(f)
df_out.reset_index(inplace=True)
return df_out
Finalmente Don Mora le pregunta si seria posible realizar un pipeline para realizar una segmentación de los clientes con los nuevos datos generados, a lo que usted responde que sí y propone la utilización de k-means para la segmentación.
A continuación siga los pasos requeridos para obtener la segmentación de clientes.
Construya una clase llamada MinMax() utilizando BaseEstimator y TransformerMixin para realizar una transformación de cada una de las columnas de un DataFrame utilizando ColumnTransformer() más tarde (tome como referencia el siguiente enlace).
Para esto considere que Min-Max escaler queda dada por la ecuación:
$$MinMax = \dfrac{x-min(x)}{max(x) - min(x)}$$Con esto buscamos que los valores que componen a las columnas se muevan en el rango de valores $[0, 1]$.
Respuesta:
Se crea una función MinMax capáz de ajustarse y utilizarla. Además, se evitan errores en el código en el caso de dividir por cero.
class MinMax(BaseEstimator, TransformerMixin):
def fit(self, X, y=None):
self.min = np.min(X,axis=0)
self.max = np.max(X,axis=0)
return self
def transform(self, X):
if (self.max == self.min).any():
return X
else:
return (X - self.min)/(self.max - self.min)
T-SNA Pipeline [1.0 puntos]¶Para comenzar introduciéndose en el uso de pipeline, decide probar realizando un pipeline enfocado en la reducción de dimensionalidad y así hacer no decepcionar a Mr. Lepin con la clusterización del modelo.
Configure un pipeline utilizando el algoritmo T-SNE sobre los datos LRMFP, donde, para la realización del pipeline considera los siguientes pasos:
df_retail_II_cleaned.pickle utilizando la función custom_features creada anteriormente, junto a FunctionTransformer(). Considere esto como el primer paso de su pipeline.ColumnTransformer() aplique el MinxMax scaler creado por usted sobre todas las columnas generadas en el paso anterior. Tras aplicar las transformaciones sobre el dataset LRMFP, gráfique las componentes obtenidas en la reducción de dimensionalidad.
Respuesta:
minmax_transformer = ColumnTransformer(
transformers=[('normalization', MinMax(), ['Length', 'Recency', 'Monetary', 'Frequency', 'Periodicity'])])
custom_features_transformer = FunctionTransformer(custom_features)
pipeline_TSNE = Pipeline([('Custom features', custom_features_transformer),('Normalize', minmax_transformer),('TSNE', TSNE())])
pipeline_TSNE.fit(df_retail)
Pipeline(steps=[('Custom features',
FunctionTransformer(func=<function custom_features at 0x0000014784716C10>)),
('Normalize',
ColumnTransformer(transformers=[('normalization', MinMax(),
['Length', 'Recency',
'Monetary', 'Frequency',
'Periodicity'])])),
('TSNE', TSNE())])
X_TSNE = pipeline_TSNE.fit_transform(df_retail)
import plotly.express as px
fig = px.scatter(x=X_TSNE[:,0], y=X_TSNE[:,1])
fig.show()
Utilizando la clase creada para escalamiento, aplique el método del codo para visualizar cual es el número de clusters que mejor se ajustan a los datos. Realice esto utilizando el algoritmo K-means dentro de un pipeline para un $k \in [1,20]$, donde k representa el número de clusters del k-means. Para la realización de esta sección y la próxima (1.3.3.2), considere los mismos pasos utilizados para el t-sne, pero permutando el algoritmo de reducción de dimensionalidad por k-means.
A través del grafico obtenido, comente y justifique que valor de k escogería para realizar el k-means.
Respuesta:
def Metodo_del_codo(X):
intertias = [
[i, KMeans(n_clusters=i, random_state=0).fit(X).inertia_]
for i in range(2, 20)
]
intertias = pd.DataFrame(intertias, columns=["n° clusters", "inertia"])
return intertias
mc_features_transformer = FunctionTransformer(Metodo_del_codo)
pipeline_metodo_Del_codo = Pipeline([('Custom features', custom_features_transformer),('Normalize', minmax_transformer),('codo',mc_features_transformer)])
pipeline_metodo_Del_codo.fit(df_retail)
intertias = pipeline_metodo_Del_codo.transform(df_retail)
px.line(intertias, x="n° clusters", y="inertia", title="Método del Codo con K-Means")
Siguiendo el "método de codo", se busca un punto de inflexión de la curva de errores vs número de clusters. El punto escogido corresponde a n° clusters = 4.
Tal como se puede apreciar en el gráfico, en el punto seleccionado ocurre un cambio brusco, "codo", lo cual indica que es un punto que minimiza tanto el error como el número de clusters.
En base a la elección de k realizada en la sección anterior, utilice este valor escogido y entrene un modelo de K-means utilizando el mismo pipeline de scikit-learn utilizado anteriormente.
Una vez ajustado los datos, genere una tabla con los promedios (o medianas) para cada uno de los atributos, agrupando estos por el clúster que pertenecen. ¿Es posible observar agrupaciones coherentes?, ¿Qué tipo de clientes posee el retail?, Justifique su respuesta y no decepcione a Mr. Lepin.
Respuesta:
pipeline_final = Pipeline([('Custom features', custom_features_transformer),('Normalize', minmax_transformer),('kmins',KMeans(n_clusters=4, random_state=0))])
pipeline_final.fit(df_retail)
Pipeline(steps=[('Custom features',
FunctionTransformer(func=<function custom_features at 0x0000014784716C10>)),
('Normalize',
ColumnTransformer(transformers=[('normalization', MinMax(),
['Length', 'Recency',
'Monetary', 'Frequency',
'Periodicity'])])),
('kmins', KMeans(n_clusters=4, random_state=0))])
X = pipeline_final.transform(df_retail)
labels = pipeline_final['kmins'].labels_
Kmins_retail = pipeline_metodo_Del_codo['Custom features'].transform(df_retail)
Kmins_retail['Cluster'] = labels
Kmins_retail = Kmins_retail[['Cluster','Length','Recency','Monetary','Frequency','Periodicity']]
Kmins_retail = Kmins_retail.groupby(by=['Cluster']).mean()
Kmins_retail.reset_index(inplace=True)
Kmins_retail
| Cluster | Length | Recency | Monetary | Frequency | Periodicity | |
|---|---|---|---|---|---|---|
| 0 | 0 | 18.751319 | 61.520723 | 367.831198 | 1.776187 | 1.847023 |
| 1 | 1 | 319.566879 | 182.003640 | 410.354868 | 10.138308 | 40.499545 |
| 2 | 2 | 19.101904 | 266.692049 | 354.724524 | 1.517357 | 1.673012 |
| 3 | 3 | 185.583920 | 154.709548 | 366.775879 | 4.382915 | 24.705528 |
¿Es posible observar agrupaciones coherentes?
Si, es posible observar agrupaciones coherentes. Se puede ver claramente como cada cluster tiene caracteristicas, o combinaciones de caracteristicas que los otros no tienen. En cada columna destaca uno o dos clusters.
¿Qué tipo de clientes posee el retail?
Cluster 0: Solo compró algunas veces en un periodo corto de tiempo. Cuando compró fue hace poco tiempo. Podemos describirlo como un "nuevo cliente". Dentro de este conjunto podrían haber futuros "clientes furtivos"
Cluster 1: Va periodicamente con una frecuencia alta. Podríamos describirlo como un "cliente frecuente"
Cluster 2: Solo compró algunas veces en un periodo corto de tiempo. Las compras no fueron recientes, así que podemos asumir que fue una compra futiva. Podemos describirlo como un "cliente furtivo".
Cluster 3: Va periodicamente pero con un periodo extenso. Podríamos describirlo como un "cliente mensual"
A modo de resumen, se tiene la siguiente descripción de cliente.
Cluster 0: Cliente nuevo
Cluster 1: Cliente frecuente
Cluster 2: Cliente furtivo
Cluster 3: Cliente mensual
Respuesta Esperada:
| Length | Recency | Frequency | Monetary | Periodicity | ||
|---|---|---|---|---|---|---|
| Cluster | ||||||
| 0 | 258.8 | 45.2 | 76.1 | 1107.7 | 107.6 | 449 |
| 1 | 76.1 | 217.6 | 45.5 | 791.7 | 14.1 | 466 |
| 2 | 368.5 | 4.8 | 2715.0 | 226621.6 | 4.2 | 4 |
| 3 | 85.3 | 45.7 | 65.8 | 1047.0 | 10.5 | 987 |
| 4 | 347.2 | 15.9 | 1658.0 | 35829.3 | 8.0 | 25 |
| 5 | 298.0 | 29.8 | 183.8 | 3639.9 | 32.0 | 1188 |
Por último, Mr. Lepin, impaciente de no entender lo que usted intenta explicarle, le solicita que por favor muestre algún resultado "visual" de los grupos encontrados.
Para esto, grafique nuevamente las características encontradas usando T-SNE (no calcule de nuevo, simplemente utilice las proyecciones encontradas) y agregue las labels calculadas con kmeans como el argumento color.
Comente: ¿Se separan bien los distintos clusters en la visualización?
Respuesta:
fig = px.scatter(x=X_TSNE[:,0], y=X_TSNE[:,1], color=labels)
fig.show()
¿Se separan bien los distintos clusters en la visualización?
Se puede ver como se separan los conjuntos en grupos. Cada cluster está separado de los otros, lo que muestra que el clustering fue hecho de forma eficiente.
Se puede apreciar que el cliente mensual está en el centro. Esta debe ser la clase más dificil de diferenciar de las otras pues comparte comportamientos con todos.
Se puede apreciar como los clientes furtivos y nuevos se separan de los clientes mensuales y frecuentes. Esto tiene sentido, pues estos pares tienen similares la frecuencia, periodicidad y fidelidad en el tiempo.
Eso ha sido todo para el lab de hoy, recuerden que el laboratorio tiene un plazo de entrega de una semana y que los días de atraso no se pueden utilizar para entregas de lab solo para tareas. Cualquier duda del laboratorio, no duden en contactarnos por mail o U-cursos.
